
package newcrunch.codeanalysis;

import compiler.ClassFileManager;
import compiler.DynamicJava;
import compiler.JavaClassObject;
import edu.umd.cs.findbugs.FindBugs2;
import java.io.*;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Scanner;
import javax.swing.JEditorPane;
import javax.swing.SwingWorker;
import newcrunch.gui.RightTab;
import newcrunch.util.Logging;
import newcrunch.util.Misc;
import newcrunch.util.PropertyLoader;

/**
 *
 * @author WL
 */
public class FindBugsPlugin 
{
    private static String rulesFile = "newcrunch/codeanalysis/findbugs.xml";
    private static String separator = System.getProperty("file.separator");
    
    static
    {
        try {
            rulesFile = PropertyLoader.getRulesProperty("findbugs");
        } catch (Exception exp) {
            Logging.log(exp);
        }
    }
    
    private static String getContents(String filename) throws FileNotFoundException
    {
        StringBuilder result = new StringBuilder();
        Scanner sc = new Scanner(new File(filename));
        while (sc.hasNextLine())
            result.append(sc.nextLine()).append("\n");
        sc.close();
        return result.toString();
    }
    
    private static File createTempDir() throws IOException
    {
        File dir = File.createTempFile("findbugs", "");
        if (!dir.delete())
            throw new IOException("Unable to delete temp file: " + dir.getAbsolutePath());
        if (!dir.mkdir())
            throw new IOException("Unable to create temp dir: " + dir.getAbsolutePath());
        dir.setReadable(true);
        dir.setWritable(true);
        dir.setExecutable(true);
        return dir;
    }
    
    private static String process(String filename, String sourceCode, SwingWorker<InfoNode, Void> worker, File tempDir) throws Exception
    {
        String tempDirName = tempDir.getAbsolutePath();
        String output = tempDirName + separator + "output.txt";
        String contents = "";
        
        String[] options = {
             "-low", "-effort:max", "-progress", "-include", rulesFile, "-output", 
                output, tempDirName
        };
        
        ByteArrayOutputStream bos = new ByteArrayOutputStream();
        
        //compiles the source code
        worker.firePropertyChange("newstatus", null, "Compiling classes...");
        ClassFileManager mgr = 
                DynamicJava.getClassManager(filename, sourceCode, bos, Arrays.asList("-g"), null);
        worker.firePropertyChange("addstatus", null, "Done");
        
        //compilation error
        if (!"".equals(bos.toString()))
        {
            worker.firePropertyChange("newstatus", null, "compilation error: " + bos.toString());
            deleteDir(tempDir);
            return "compilation error: " + bos.toString();
        }
        
        //writes the compiled classes to disk
        for (JavaClassObject obj : mgr.getClassObjects())
        {
            try {
                String classFileName = tempDirName + obj.getName();
                createPackageDir(tempDir, obj.getName());
                FileOutputStream outputStream = 
                        new FileOutputStream(classFileName);
                worker.firePropertyChange("newstatus", null, "Writing " + obj.getName() + "...");
                outputStream.write(obj.getBytes());
                outputStream.close();
            } catch (Exception exp) {
                Logging.log(exp);
            }
        }

        //process the compiled classes
        worker.firePropertyChange("newstatus", null, "Running findbugs...");
        ByteArrayOutputStream nbos = new ByteArrayOutputStream();
        FindBugs2.main(options, new FindBugsOutStream(worker, nbos));

        //retrieves the results produced by findbugs
        contents = getContents(output);
        
        deleteDir(tempDir);
        return contents;
    }
    
    /**
     * Creates all the intermediate directories if the class
     * is specified to be inside a package.
     * @param rootDir
     * @param className 
     */
    private static void createPackageDir(File rootDir, String className) throws IOException
    {
        String[] folders = className.split("/");
        File parentFolder = rootDir;
        
        //exclude the last element, which is the class itself
        for (int i = 0; i < folders.length - 1; i++)
        {
            File currFolder = new File(parentFolder, folders[i]);
            if (!currFolder.exists() && !currFolder.mkdir())
                throw new IOException("Unable to create directory " + currFolder.getAbsolutePath());
            parentFolder = currFolder;
        }
    }
    
    /**
     * Recursively deletes everything in the directory
     * before deleting the current directory.
     * @param dir 
     */
    private static void deleteDir(File dir)
    {
        if (dir == null || !dir.exists() || !dir.isDirectory())
            return;
        
        for (File file : dir.listFiles())
        {
            String filename = file.getName();
            if (!".".equals(filename) && !"..".equals(filename))
            {
                if (file.isDirectory())
                    deleteDir(file);
                
                else if (!file.delete())
                    Logging.log(new IOException("Unable to delete " + file.getAbsolutePath()));
            }
        }
        
        //delete itself
        if (!dir.delete())
            Logging.log(new IOException("Unable to delete " + dir.getAbsolutePath()));
    }
    
    public static InfoNode runFindBugs(RightTab tab, SwingWorker<InfoNode, Void> worker) throws Exception
    {
        String output = "";
        File tempDir = null;
        try {
            tempDir = createTempDir();
            output = process(tab.getFilename(), tab.getText(), worker, tempDir);
        } catch (Exception exp) {
            Logging.log(exp);
        } finally {
            deleteDir(tempDir);
        }
        
        InfoNode fbNode = new InfoNode("Findbugs");
        
        if (output.startsWith("compilation error"))
            fbNode.add(new InfoNode(output));
        
        else
            parse(fbNode, output, tab.getEditor());
        
        return fbNode;
    }
    
    /**
     * Parses the output string and creates nodes to add to the root node.
     * @param rootNode
     * @param output 
     */
    private static void parse(InfoNode rootNode, String output, JEditorPane editor)
    {
        //no errors
        if (output.trim().isEmpty())
            return;
        
        String[] errorLines = output.split("\n");
        HashMap<String, InfoNode> mapping = new HashMap<String, InfoNode>();
        
        for (String line : errorLines)
        {
            int start = line.indexOf(':') + 1;
            int end = line.indexOf("At");
            String msg = line.substring(start, end).trim();
            
            //settles the header node
            InfoNode headerNode = null;
            
            if ((headerNode = mapping.get(msg)) == null)
            {
                headerNode = new InfoNode(msg);
                mapping.put(msg, headerNode);
            }
            
            int startPos = 0;
            int endPos = 0;
            
            //creates the code node
            if (line.indexOf("[lines") != -1)
            {
                int dash = line.lastIndexOf("-");
                int startFirstNum = line.indexOf("[lines") + 7;
                int endSecondNum = line.length() - 1;
                
                int startRow = Integer.parseInt(line.substring(startFirstNum, dash));
                int endRow = Integer.parseInt(line.substring(dash + 1, endSecondNum));
                startPos = Misc.rowToPosition(editor, startRow - 1);
                endPos = Misc.rowToPosition(editor, endRow);
            }
            else
            {
                int startFirstNum = line.indexOf("[line") + 6;
                int row = Integer.parseInt(line.substring(startFirstNum, line.length() - 1));
                startPos = Misc.rowToPosition(editor, row - 1);
                endPos = Misc.rowToPosition(editor, row);
            }
            
            String display = editor.getText().substring(startPos, endPos) + " " +
                                line.substring(line.indexOf("[line")).trim();
            headerNode.add(new InfoNode(display.trim(), startPos, endPos));
        }
        
        //sort the headings
        String[] msgs = mapping.keySet().toArray(new String[mapping.keySet().size()]);
        Arrays.sort(msgs);
        
        //add the headings
        for (String msg : msgs)
            rootNode.add(mapping.get(msg));
    }
}
